// eslint-disable-next-line import/no-named-default
import type { default as Transport } from '@ledgerhq/hw-transport'
// eslint-disable-next-line import/no-named-default
import type { default as TransportWebHID } from '@ledgerhq/hw-transport-webhid'
import type { WalletName } from '@solana/wallet-adapter-base'
import {
  BaseSignerWalletAdapter,
  WalletConnectionError,
  WalletDisconnectedError,
  WalletDisconnectionError,
  WalletLoadError,
  WalletNotConnectedError,
  WalletNotReadyError,
  WalletPublicKeyError,
  WalletReadyState,
  WalletSignTransactionError
} from '@solana/wallet-adapter-base'
import type { PublicKey, Transaction, TransactionVersion, VersionedTransaction } from '@solana/web3.js'
import { getDerivationPath, getPublicKey, signTransaction } from './util'
import './polyfill'

export interface LedgerWalletAdapterConfig {
  derivationPath?: Buffer
}

export const LedgerWalletName = 'Ledger' as WalletName<'Ledger'>

export class LedgerWalletAdapter extends BaseSignerWalletAdapter {
  name = LedgerWalletName

  url = 'https://ledger.com'

  icon =
    ''

  supportedTransactionVersions: ReadonlySet<TransactionVersion> = new Set(['legacy', 0])

  private _derivationPath: Buffer

  private _connecting: boolean

  private _transport: Transport | null

  private _publicKey: PublicKey | null

  private _readyState: WalletReadyState =
    typeof window === 'undefined' || typeof document === 'undefined' || typeof navigator === 'undefined' || !(navigator as any).hid
      ? WalletReadyState.Unsupported
      : WalletReadyState.Loadable

  constructor(config: LedgerWalletAdapterConfig = {}) {
    super()
    this._derivationPath = config.derivationPath || getDerivationPath(0, 0)
    this._connecting = false
    this._transport = null
    this._publicKey = null
  }

  get publicKey() {
    return this._publicKey
  }

  get connecting() {
    return this._connecting
  }

  get readyState() {
    return this._readyState
  }

  async connect(): Promise<void> {
    try {
      if (this.connected || this.connecting) return
      if (this._readyState !== WalletReadyState.Loadable) throw new WalletNotReadyError()

      this._connecting = true

      let TransportWebHIDClass: typeof TransportWebHID
      try {
        TransportWebHIDClass = (await import('@ledgerhq/hw-transport-webhid')).default
      } catch (error: any) {
        throw new WalletLoadError(error?.message, error)
      }

      let transport: Transport
      try {
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        transport = await TransportWebHIDClass.create()
      } catch (error: any) {
        throw new WalletConnectionError(error?.message, error)
      }

      let publicKey: PublicKey
      try {
        publicKey = await getPublicKey(transport, this._derivationPath)
      } catch (error: any) {
        throw new WalletPublicKeyError(error?.message, error)
      }

      transport.on('disconnect', this._disconnected)

      this._transport = transport
      this._publicKey = publicKey

      this.emit('connect', publicKey)
    } catch (error: any) {
      this.emit('error', error)
      throw error
    } finally {
      this._connecting = false
    }
  }

  async disconnect(): Promise<void> {
    const transport = this._transport
    if (transport) {
      transport.off('disconnect', this._disconnected)

      this._transport = null
      this._publicKey = null

      try {
        await transport.close()
      } catch (error: any) {
        this.emit('error', new WalletDisconnectionError(error?.message, error))
      }
    }

    this.emit('disconnect')
  }

  async signTransaction<T extends Transaction | VersionedTransaction>(transaction: T): Promise<T> {
    try {
      const transport = this._transport
      const publicKey = this._publicKey
      if (!transport || !publicKey) throw new WalletNotConnectedError()

      try {
        const signature = await signTransaction(transport, transaction, this._derivationPath)
        transaction.addSignature(publicKey, signature as any)
      } catch (error: any) {
        throw new WalletSignTransactionError(error?.message, error)
      }

      return transaction
    } catch (error: any) {
      this.emit('error', error)
      throw error
    }
  }

  private _disconnected = () => {
    const transport = this._transport
    if (transport) {
      transport.off('disconnect', this._disconnected)

      this._transport = null
      this._publicKey = null

      this.emit('error', new WalletDisconnectedError())
      this.emit('disconnect')
    }
  }
}
